/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.core;

import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.security.SecurityUtil;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.log.SystemLogHandler;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.modeler.Util;
import org.apache.tomcat.util.res.StringManager;

import javax.management.ObjectName;
import javax.naming.NamingException;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * Implementation of a <code>javax.servlet.FilterConfig</code> useful in
 * managing the filter instances instantiated when a web application
 * is first started.
 *
 * @author Craig R. McClanahan
 */
public final class ApplicationFilterConfig implements FilterConfig, Serializable {

	static final StringManager sm =
			StringManager.getManager(Constants.Package);
	private static final long serialVersionUID = 1L;
	private static final org.apache.juli.logging.Log log =
			LogFactory.getLog(ApplicationFilterConfig.class);

	/**
	 * Empty String collection to serve as the basis for empty enumerations.
	 */
	private static final List<String> emptyString = Collections.emptyList();

	// ----------------------------------------------------------- Constructors
	/**
	 * The <code>FilterDef</code> that defines our associated Filter.
	 */
	private final FilterDef filterDef;


	// ----------------------------------------------------- Instance Variables
	/**
	 * The Context with which we are associated.
	 */
	private transient Context context = null;

	/**
	 * The application Filter we are configured for.
	 */
	private transient Filter filter = null;
	/**
	 * the InstanceManager used to create and destroy filter instances.
	 */
	private transient InstanceManager instanceManager;
	/**
	 * JMX registration name
	 */
	private ObjectName oname;

	/**
	 * Construct a new ApplicationFilterConfig for the specified filter
	 * definition.
	 *
	 * @param context   The context with which we are associated
	 * @param filterDef Filter definition for which a FilterConfig is to be
	 *                  constructed
	 * @throws ClassCastException        if the specified class does not implement
	 *                                   the <code>javax.servlet.Filter</code> interface
	 * @throws ClassNotFoundException    if the filter class cannot be found
	 * @throws IllegalAccessException    if the filter class cannot be
	 *                                   publicly instantiated
	 * @throws InstantiationException    if an exception occurs while
	 *                                   instantiating the filter object
	 * @throws ServletException          if thrown by the filter's init() method
	 * @throws NamingException
	 * @throws InvocationTargetException
	 */
	ApplicationFilterConfig(Context context, FilterDef filterDef)
			throws ClassCastException, ClassNotFoundException,
			IllegalAccessException, InstantiationException,
			ServletException, InvocationTargetException, NamingException {

		super();

		this.context = context;
		this.filterDef = filterDef;
		// Allocate a new filter instance if necessary
		if (filterDef.getFilter() == null) {
			getFilter();
		} else {
			this.filter = filterDef.getFilter();
			getInstanceManager().newInstance(filter);
			initFilter();
		}
	}

	// --------------------------------------------------- FilterConfig Methods

	/**
	 * Return the name of the filter we are configuring.
	 */
	@Override
	public String getFilterName() {
		return (filterDef.getFilterName());
	}

	/**
	 * Return the class of the filter we are configuring.
	 */
	public String getFilterClass() {
		return filterDef.getFilterClass();
	}

	/**
	 * Return a <code>String</code> containing the value of the named
	 * initialization parameter, or <code>null</code> if the parameter
	 * does not exist.
	 *
	 * @param name Name of the requested initialization parameter
	 */
	@Override
	public String getInitParameter(String name) {

		Map<String, String> map = filterDef.getParameterMap();
		if (map == null) {
			return (null);
		}

		return map.get(name);

	}

	/**
	 * Return an <code>Enumeration</code> of the names of the initialization
	 * parameters for this Filter.
	 */
	@Override
	public Enumeration<String> getInitParameterNames() {
		Map<String, String> map = filterDef.getParameterMap();

		if (map == null) {
			return Collections.enumeration(emptyString);
		}

		return Collections.enumeration(map.keySet());
	}

	/**
	 * Return the ServletContext of our associated web application.
	 */
	@Override
	public ServletContext getServletContext() {

		return this.context.getServletContext();

	}

	/**
	 * Return a String representation of this object.
	 */
	@Override
	public String toString() {

		StringBuilder sb = new StringBuilder("ApplicationFilterConfig[");
		sb.append("name=");
		sb.append(filterDef.getFilterName());
		sb.append(", filterClass=");
		sb.append(filterDef.getFilterClass());
		sb.append("]");
		return (sb.toString());

	}

	// --------------------------------------------------------- Public Methods

	public Map<String, String> getFilterInitParameterMap() {
		return Collections.unmodifiableMap(filterDef.getParameterMap());
	}

	// -------------------------------------------------------- Package Methods

	/**
	 * Return the application Filter we are configured for.
	 *
	 * @throws ClassCastException        if the specified class does not implement
	 *                                   the <code>javax.servlet.Filter</code> interface
	 * @throws ClassNotFoundException    if the filter class cannot be found
	 * @throws IllegalAccessException    if the filter class cannot be
	 *                                   publicly instantiated
	 * @throws InstantiationException    if an exception occurs while
	 *                                   instantiating the filter object
	 * @throws ServletException          if thrown by the filter's init() method
	 * @throws NamingException
	 * @throws InvocationTargetException
	 */
	Filter getFilter() throws ClassCastException, ClassNotFoundException,
			IllegalAccessException, InstantiationException, ServletException,
			InvocationTargetException, NamingException {

		// Return the existing filter instance, if any
		if (this.filter != null)
			return (this.filter);

		// Identify the class loader we will be using
		String filterClass = filterDef.getFilterClass();
		this.filter = (Filter) getInstanceManager().newInstance(filterClass);

		initFilter();

		return (this.filter);

	}

	private void initFilter() throws ServletException {
		if (context instanceof StandardContext &&
				context.getSwallowOutput()) {
			try {
				SystemLogHandler.startCapture();
				filter.init(this);
			} finally {
				String capturedlog = SystemLogHandler.stopCapture();
				if (capturedlog != null && capturedlog.length() > 0) {
					getServletContext().log(capturedlog);
				}
			}
		} else {
			filter.init(this);
		}

		// Expose filter via JMX
		registerJMX();
	}

	/**
	 * Return the filter definition we are configured for.
	 */
	FilterDef getFilterDef() {

		return (this.filterDef);

	}

	/**
	 * Release the Filter instance associated with this FilterConfig,
	 * if there is one.
	 */
	void release() {

		unregisterJMX();

		if (this.filter != null) {
			try {
				if (Globals.IS_SECURITY_ENABLED) {
					try {
						SecurityUtil.doAsPrivilege("destroy", filter);
					} finally {
						SecurityUtil.remove(filter);
					}
				} else {
					filter.destroy();
				}
			} catch (Throwable t) {
				ExceptionUtils.handleThrowable(t);
				context.getLogger().error(sm.getString(
						"applicationFilterConfig.release",
						filterDef.getFilterName(),
						filterDef.getFilterClass()), t);
			}
			if (!context.getIgnoreAnnotations()) {
				try {
					((StandardContext) context).getInstanceManager().destroyInstance(this.filter);
				} catch (Exception e) {
					Throwable t = ExceptionUtils
							.unwrapInvocationTargetException(e);
					ExceptionUtils.handleThrowable(t);
					context.getLogger().error(
							sm.getString("applicationFilterConfig.preDestroy",
									filterDef.getFilterName(), filterDef.getFilterClass()), t);
				}
			}
		}
		this.filter = null;

	}


	// -------------------------------------------------------- Private Methods

	private InstanceManager getInstanceManager() {
		if (instanceManager == null) {
			if (context instanceof StandardContext) {
				instanceManager = ((StandardContext) context).getInstanceManager();
			} else {
				instanceManager = new DefaultInstanceManager(null,
						new HashMap<String, Map<String, String>>(),
						context,
						getClass().getClassLoader());
			}
		}
		return instanceManager;
	}

	private void registerJMX() {
		String parentName = context.getName();
		if (!parentName.startsWith("/")) {
			parentName = "/" + parentName;
		}

		String hostName = context.getParent().getName();
		hostName = (hostName == null) ? "DEFAULT" : hostName;

		// domain == engine name
		String domain = context.getParent().getParent().getName();

		String webMod = "//" + hostName + parentName;
		String onameStr = null;
		String filterName = filterDef.getFilterName();
		if (Util.objectNameValueNeedsQuote(filterName)) {
			filterName = ObjectName.quote(filterName);
		}
		if (context instanceof StandardContext) {
			StandardContext standardContext = (StandardContext) context;
			onameStr = domain + ":j2eeType=Filter,name=" + filterName +
					",WebModule=" + webMod + ",J2EEApplication=" +
					standardContext.getJ2EEApplication() + ",J2EEServer=" +
					standardContext.getJ2EEServer();
		} else {
			onameStr = domain + ":j2eeType=Filter,name=" + filterName +
					",WebModule=" + webMod;
		}
		try {
			oname = new ObjectName(onameStr);
			Registry.getRegistry(null, null).registerComponent(this, oname,
					null);
		} catch (Exception ex) {
			log.info(sm.getString("applicationFilterConfig.jmxRegisterFail",
					getFilterClass(), getFilterName()), ex);
		}
	}

	private void unregisterJMX() {
		// unregister this component
		if (oname != null) {
			try {
				Registry.getRegistry(null, null).unregisterComponent(oname);
				if (log.isDebugEnabled())
					log.debug(sm.getString(
							"applicationFilterConfig.jmxUnregister",
							getFilterClass(), getFilterName()));
			} catch (Exception ex) {
				log.error(sm.getString(
						"applicationFilterConfig.jmxUnregisterFail",
						getFilterClass(), getFilterName()), ex);
			}
		}

	}
}
